/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022.
 * Desription: support RTOS tinysleep feature
 * Author: lincheng <lincheng8@huawei.com>
 * Create: 2022-03-09
 */

#include <linux/syscalls.h>

#ifdef CONFIG_RTOS_TINYSLEEP
#include <asm/mwait.h>
#include <linux/delay.h>
#include <linux/rtos/tinysleep.h>
#include <uapi/linux/sched/types.h>
#ifdef CONFIG_RTOS_MICROSTATE_ACCT
#include <linux/msa.h>
#include <linux/task_struct_extend.h>
#endif

#define TINYSLEEP_INSLEEP		1
#define TINYSLEEP_UNSLEEP		0
#define TINYSLEEP_CACHE_LINE_OFFSET	4

#define tinysleep_info(arg, ...)	pr_info("tinysleep: "arg, ##__VA_ARGS__)
#define tinysleep_err(arg, ...)		pr_err("tinysleep: "arg, ##__VA_ARGS__)

struct cpu_tinysleep_status {
	unsigned long mwait_status;
};

static DEFINE_PER_CPU(struct cpu_tinysleep_status, tinysleep_status);
static DEFINE_PER_CPU(struct task_struct *, ktinysleep_schedd);
static int tinysleep_scheduler_interval = 10; /* default value is 10ms */
static unsigned long long tinysleep_wait_addr[NR_CPUS << TINYSLEEP_CACHE_LINE_OFFSET] __attribute__((__aligned__(128)));

bool tinysleep_test_and_clear(int inirq)
{
	struct cpu_tinysleep_status *status;

	preempt_disable();

	status = &per_cpu(tinysleep_status, smp_processor_id());
	if (status->mwait_status == TINYSLEEP_INSLEEP) {
		if (!inirq)
			status->mwait_status = TINYSLEEP_UNSLEEP;
		preempt_enable();
		return true;
	}

	preempt_enable();

	return false;
}

unsigned long long *tinysleep_get_wait_addr(unsigned int cpu)
{
	if (cpu >= NR_CPUS) {
		tinysleep_err("cpu %u is >= %u\n", cpu, NR_CPUS);
		return NULL;
	}

	if (!cpu_online(cpu)) {
		tinysleep_err("cpu %u is not online\n", cpu);
		return NULL;
	}

	return &(tinysleep_wait_addr[cpu << TINYSLEEP_CACHE_LINE_OFFSET]);
}
EXPORT_SYMBOL_NS(tinysleep_get_wait_addr, HW_RTOS_NS);

void tinysleep_wakeup(unsigned int cpu)
{
	if (cpu >= NR_CPUS) {
		tinysleep_err("wakeup error: cpu %u is >= %u\n", cpu, NR_CPUS);
		return;
	}

	if (!cpu_online(cpu)) {
		tinysleep_err("wakeup error: cpu %u is not online\n", cpu);
		return;
	}

	tinysleep_wait_addr[cpu << TINYSLEEP_CACHE_LINE_OFFSET] = 0x1;
}
EXPORT_SYMBOL_NS(tinysleep_wakeup, HW_RTOS_NS);

static int do_mwait(void)
{
	unsigned int cpu;
	unsigned long long *wait_addr;
	struct cpu_tinysleep_status *status;
#ifdef CONFIG_RTOS_MICROSTATE_ACCT
	struct rtos_task_struct *rtos_task_current;
#endif

	preempt_disable();

	cpu = smp_processor_id();
	wait_addr = &tinysleep_wait_addr[cpu << TINYSLEEP_CACHE_LINE_OFFSET];

#ifdef CONFIG_RTOS_MICROSTATE_ACCT
	rtos_task_current = task_to_rtos_task(current);
	rtos_task_current->microstates->cur_state = MSA_INTERRUPTIBLE_SLEEP;
#endif

	status = &per_cpu(tinysleep_status, cpu);
	status->mwait_status = TINYSLEEP_INSLEEP;

	if (static_cpu_has_bug(X86_BUG_CLFLUSH_MONITOR)) {
		mb();
		clflush((void *)wait_addr);
		mb();
	}

	__monitor((void *)wait_addr, 0, 0);
	smp_mb();
	__sti_mwait(0, 0);

	preempt_enable();

	return 0;
}

SYSCALL_DEFINE0(tinysleep)
{
	return do_mwait();
}

static int tinysleep_scheduler(void *data)
{
	struct sched_param param = { .sched_priority = MAX_RT_PRIO - 1 };

	sched_setscheduler(current, SCHED_FIFO, &param);
	while (!kthread_should_stop())
		msleep_interruptible((unsigned int)tinysleep_scheduler_interval);

	return 0;
}

static int tinysleep_scheduler_task_create(unsigned int cpu)
{
	struct task_struct *p;

	p = per_cpu(ktinysleep_schedd, cpu);
	if (!p) {
		p = kthread_create(tinysleep_scheduler, (void *)(&cpu), "ktinyschedd/%u", cpu);
		if (IS_ERR(p)) {
			tinysleep_err("ktinyschedd for cpu(%u) failed\n", cpu);
			return 1;
		}
		kthread_bind(p, cpu);
		per_cpu(ktinysleep_schedd, cpu) = p;
	}
	wake_up_process(p);
	return 0;
}

static int tinysleep_scheduler_task_destroy(unsigned int cpu)
{
	struct task_struct *p;

	p = per_cpu(ktinysleep_schedd, cpu);
	if (p) {
		per_cpu(ktinysleep_schedd, cpu) = NULL;
		kthread_stop(p);
	}
	return 0;
}

static int __init spawn_tinysleep_scheduler_task(void)
{
	int ret;

	if (tinysleep_scheduler_interval == 0) {
		tinysleep_info("we will not use tinysleep_scheduler to keep schedule\n");
		return 0;
	}

	tinysleep_info("we will use tinysleep_scheduler to keep schedule\n");
	ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "tinysleep scheduler online",
			tinysleep_scheduler_task_create, tinysleep_scheduler_task_destroy);
	WARN_ON(ret < 0);

	return 0;
}
early_initcall(spawn_tinysleep_scheduler_task);

static int __init tinysleep_scheduler_setup(char *str)
{
	int val;

	get_option(&str, &val);
	if (val > INT_MAX) {
		val = INT_MAX;
		tinysleep_err("tinysleep_scheduler_interval=%s is out of range, use INT_MAX\n", str);
	}

	tinysleep_info("tinysleep_scheduler_interval=%d, tinysleep scheduler is %s\n", val, val == 0 ? "disabled" : "enabled");
	tinysleep_scheduler_interval = val;

	return 1;
}
early_param("tinysleep_scheduler_interval", tinysleep_scheduler_setup);

#else /* CONFIG_RTOS_TINYSLEEP */
SYSCALL_DEFINE0(tinysleep)
{
	return -ENOSYS;
}
#endif /* CONFIG_RTOS_TINYSLEEP */
